/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * SPDX-License-Identifier: EPL-1.0
 ********************************************************************************/
package org.eclipse.ceylon.ide.eclipse.code.explorer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.internal.corext.refactoring.reorg.ReorgUtils;
import org.eclipse.jdt.internal.ui.dnd.JdtViewerDropAdapter;
import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs;
import org.eclipse.jdt.internal.ui.workingsets.WorkingSetModel;
import org.eclipse.jface.util.TransferDropTargetListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.views.navigator.LocalSelectionTransfer;

@SuppressWarnings("deprecation")
class WorkingSetDropAdapter extends JdtViewerDropAdapter implements TransferDropTargetListener {

    private PackageExplorerPart fPackageExplorer;

    private IStructuredSelection fSelection;
    private Object[] fElementsToAdds;
    private Set<IAdaptable> fCurrentElements;
    private IWorkingSet fWorkingSet;

    private int fLocation;

    public WorkingSetDropAdapter(PackageExplorerPart part) {
        super(part.getTreeViewer());
        fPackageExplorer= part;

        fLocation= -1;

        setScrollEnabled(true);
        setExpandEnabled(true);
        setFeedbackEnabled(false);
    }

    //---- TransferDropTargetListener interface ---------------------------------------

    /**
     * {@inheritDoc}
     */
    public Transfer getTransfer() {
        return LocalSelectionTransfer.getInstance();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isEnabled(DropTargetEvent event) {
        Object target= event.item != null ? event.item.getData() : null;
        if (target == null)
            return false;
        ISelection selection= LocalSelectionTransfer.getInstance().getSelection();
        if (!isValidSelection(selection)) {
            return false;
        }
        if (!isValidTarget(target))
            return false;

        initializeState(target, selection);
        return true;
    }

    //---- Actual DND -----------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validateDrop(Object target, int operation, TransferData transferType) {
        return determineOperation(target, operation, transferType, DND.DROP_MOVE | DND.DROP_LINK | DND.DROP_COPY) != DND.DROP_NONE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int determineOperation(Object target, int operation, TransferData transferType, int operations) {
        switch(operation) {
            case DND.DROP_DEFAULT:
            case DND.DROP_COPY:
            case DND.DROP_MOVE:
                return validateTarget(target, operation);
            default:
                return DND.DROP_NONE;
        }

    }

    private int validateTarget(Object target, int operation) {
        setFeedbackEnabled(false);
        setScrollEnabled(true);
        setExpandEnabled(true);
        if (!isValidTarget(target))
            return DND.DROP_NONE;
        ISelection s= LocalSelectionTransfer.getInstance().getSelection();
        if (!isValidSelection(s)) {
            return DND.DROP_NONE;
        }

        initializeState(target, s);

        if (isWorkingSetSelection()) {
            setExpandEnabled(false);
            if ((getCurrentLocation() == LOCATION_BEFORE || getCurrentLocation() == LOCATION_AFTER) &&
                    !fPackageExplorer.getWorkingSetModel().isSortingEnabled()) {
                setFeedbackEnabled(true);
                return DND.DROP_MOVE;
            }
            return DND.DROP_NONE;
        } else {
            if (isOthersWorkingSet(fWorkingSet) && operation == DND.DROP_COPY)
                return DND.DROP_NONE;

            List<IJavaElement> realJavaElements= new ArrayList<IJavaElement>();
            List<IResource> realResource= new ArrayList<IResource>();
            ReorgUtils.splitIntoJavaElementsAndResources(fElementsToAdds, realJavaElements, realResource);
            if (fElementsToAdds.length != realJavaElements.size() + realResource.size())
                return DND.DROP_NONE;
            for (Iterator<IJavaElement> iter= realJavaElements.iterator(); iter.hasNext();) {
                IJavaElement element= iter.next();
                if (ReorgUtils.containsElementOrParent(fCurrentElements, element))
                    return DND.DROP_NONE;
            }
            for (Iterator<IResource> iter= realResource.iterator(); iter.hasNext();) {
                IResource element= iter.next();
                if (ReorgUtils.containsElementOrParent(fCurrentElements, element))
                    return DND.DROP_NONE;
            }
            if (!(fSelection instanceof ITreeSelection)) {
                return DND.DROP_COPY;
            }
            ITreeSelection treeSelection= (ITreeSelection)fSelection;
            TreePath[] paths= treeSelection.getPaths();
            for (int i= 0; i < paths.length; i++) {
                TreePath path= paths[i];
                if (path.getSegmentCount() != 2)
                    return DND.DROP_COPY;
                if (!(path.getSegment(0) instanceof IWorkingSet))
                    return DND.DROP_COPY;
                if (paths.length == 1) {
                    IWorkingSet ws= (IWorkingSet)path.getSegment(0);
                    if (isOthersWorkingSet(ws))
                        return DND.DROP_MOVE;
                }
            }
        }
        if (operation == DND.DROP_DEFAULT)
            return DND.DROP_MOVE;
        return operation;
    }

    private boolean isValidTarget(Object target) {
        return target instanceof IWorkingSet;
    }

    private boolean isValidSelection(ISelection selection) {
        return selection instanceof IStructuredSelection;
    }

    private boolean isOthersWorkingSet(IWorkingSet ws) {
        return IWorkingSetIDs.OTHERS.equals(ws.getId());
    }

    private void initializeState(Object target, ISelection s) {
        fWorkingSet= (IWorkingSet)target;
        fSelection= (IStructuredSelection)s;
        fElementsToAdds= fSelection.toArray();
        fCurrentElements= new HashSet<IAdaptable>(Arrays.asList(fWorkingSet.getElements()));
    }

    private boolean isWorkingSetSelection() {
        for (int i= 0; i < fElementsToAdds.length; i++) {
            if (!(fElementsToAdds[i] instanceof IWorkingSet))
                return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean performDrop(Object data) {
        if (isWorkingSetSelection()) {
            performWorkingSetReordering();
        } else {
            performElementRearrange(getCurrentOperation());
        }
        return true;
    }

    private void performWorkingSetReordering() {
        WorkingSetModel model= fPackageExplorer.getWorkingSetModel();
        List<IWorkingSet> allWorkingSets= new ArrayList<IWorkingSet>(Arrays.asList(model.getAllWorkingSets()));
        int index= allWorkingSets.indexOf(fWorkingSet);
        if (index != -1) {
            if (getCurrentLocation() == LOCATION_AFTER)
                index++;
            List<IWorkingSet> result= new ArrayList<IWorkingSet>(allWorkingSets.size());
            @SuppressWarnings("unchecked") // isWorkingSetSelection() ensures that all elements are IWorkingSets
            List<IWorkingSet> selected= new ArrayList<IWorkingSet>((List<IWorkingSet>) (List<?>) Arrays.asList(fElementsToAdds));
            List<IWorkingSet> activeWorkingSets= new ArrayList<IWorkingSet>(Arrays.asList(model.getActiveWorkingSets()));
            List<IWorkingSet> active= new ArrayList<IWorkingSet>(activeWorkingSets.size());
            for (int i= 0; i < allWorkingSets.size(); i++) {
                if (i == index) {
                    result.addAll(selected);
                    active.addAll(selected);
                }
                IWorkingSet element= allWorkingSets.get(i);
                if (!selected.contains(element)) {
                    result.add(element);
                    if (activeWorkingSets.contains(element))
                        active.add(element);
                }
            }
            if (index == allWorkingSets.size()) {
                result.addAll(selected);
                active.addAll(selected);
            }
            model.setWorkingSets(result.toArray(new IWorkingSet[result.size()]), model.isSortingEnabled(), active.toArray(new IWorkingSet[active.size()]));
        }
    }

    private void performElementRearrange(int eventDetail) {
        // only move if target isn't the other working set. If this is the case
        // the move will happenn automatically by refreshing the other working set
        if (!isOthersWorkingSet(fWorkingSet)) {
            List<Object> elements= new ArrayList<Object>(Arrays.asList(fWorkingSet.getElements()));
            elements.addAll(Arrays.asList(fElementsToAdds));
            fWorkingSet.setElements(elements.toArray(new IAdaptable[elements.size()]));
        }
        if (eventDetail == DND.DROP_MOVE) {
            ITreeSelection treeSelection= (ITreeSelection)fSelection;
            Map<IWorkingSet, List<Object>> workingSets= groupByWorkingSets(treeSelection.getPaths());
            for (Iterator<IWorkingSet> iter= workingSets.keySet().iterator(); iter.hasNext();) {
                IWorkingSet ws= iter.next();
                List<Object> toRemove= workingSets.get(ws);
                List<IAdaptable> currentElements= new ArrayList<IAdaptable>(Arrays.asList(ws.getElements()));
                currentElements.removeAll(toRemove);
                ws.setElements(currentElements.toArray(new IAdaptable[currentElements.size()]));
            }
        }
    }

    private Map<IWorkingSet, List<Object>> groupByWorkingSets(TreePath[] paths) {
        Map<IWorkingSet, List<Object>> result= new HashMap<IWorkingSet, List<Object>>();
        for (int i= 0; i < paths.length; i++) {
            TreePath path= paths[i];
            IWorkingSet ws= (IWorkingSet)path.getSegment(0);
            List<Object> l= result.get(ws);
            if (l == null) {
                l= new ArrayList<Object>();
                result.put(ws, l);
            }
            l.add(path.getSegment(1));
        }
        return result;
    }

    //---- test methods for JUnit test since DnD is hard to simulate

    public int internalTestValidateTarget(Object target, int operation) {
        return validateTarget(target, operation);
    }

    public void internalTestDrop(Object target, int eventDetail) {
        if (isWorkingSetSelection()) {
            performWorkingSetReordering();
        } else {
            performElementRearrange(eventDetail);
        }
    }

    public void internalTestSetLocation(int location) {
        fLocation= location;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int getCurrentLocation() {
        if (fLocation == -1)
            return super.getCurrentLocation();

        return fLocation;
    }
}